iT邦幫忙

2024 iThome 鐵人賽

DAY 29
0

如果想要在 Spring Boot 中結合 Redis 進行應用,需要先進行一些基本配置:

Step 1. 於 pom.xml 內加入 Redis 的依賴項目

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version><!-- 請自行選擇適用版本 --></version>
</dependency>

spring-boot-starter-data-redis 提供了對 Redis 的支援,故需要於 pom.xml 內加入此依賴項目,此套件提供了 RedisConnectionFactory實例用於建立和管理程式與Redis的連接、StringRedisTemplate、RedisTemplate 兩者皆是操作Redis的模組,提供了豐富的方法來執行 Redis 的操作,StringRedisTemplate 作為 RedisTemplate 的子類專門用來操作字串類型的資料。

Step 2. 於 application.properties 內設定 Redis 的連接方式

spring.redis.host = 127.0.0.1
spring.redis.port = 6379
spring.redis.password = 
spring.redis.database = 1

Redis 默認的通訊埠號為6379。Redis 的密碼預設為空。此外,Redis 預設提供了16個 DataBase,每個資料庫都是以數字命名(0-15),且資料庫間是相互獨立的,如附圖所示。
https://ithelp.ithome.com.tw/upload/images/20241013/20168753KZlF0kd6ll.jpg

實作範例

在完成上述步驟後,接下來會透過一個實作來更進一步了解 Spring Boot 結合 Redis 的使用方式。此次實作預計做一個計分器,我們將設計一個 API 接口,任何人都可以透過此支 API 將積分累加。
首先在 Service 內定義數字增量的方法:

@Service("RedisOperatorService")
public class RedisOperatorService {

    private RedisTemplate redisTemplate;

    private static final String REDIS_INCR_KEY = "incrKey";

    RedisOperatorService(RedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void incr(int score) {
        try {
            int currentValue = 0;
            Object object = redisTemplate.opsForValue().get(REDIS_INCR_KEY);
            if (object == null) {
                currentValue = add(currentValue, score);
            } else {
                currentValue = add(Integer.valueOf(object.toString()), score);
            }
            redisTemplate.opsForValue().set(REDIS_INCR_KEY, String.valueOf(currentValue));
            logger.info("currentValue = " + currentValue);
        } catch (Exception e) {
            throw new RuntimeException("Failed to increment value in Redis", e);
        }
    }
    
    public int add(int a, int b) {
        return a + b;
    }
}

定義了一個變數 REDIS_INCR_KEY 其值為 incrKey 用於取得存於 Redis 內的指定資料。並透過建構式注入方式將 RedisTemplate 注入到 RedisOperatorService 內用於操作Redis。另外,定義了一個 incr() 方法,在該方法中我們透過呼叫 add() 方法將 API 傳入的值與 Redis 內的值相加後再存回 Redis 內。

在 Controller 中定義一個API接口如下:

@RequestMapping(value = "/increment", method = {RequestMethod.POST})
@ResponseBody
public void incrementSerialNumber() {

    redisOperatorService.incr();

}

我們可以透過此支 API 將 Redis 內的資料進行遞增的動作。

這次的測試會使用 JMeter 進行操作,它是一個開源的壓力測試工具,主要用於測試 API 的性能和負荷量。日後有機會的話會再進行介紹,大家可以先 Google JMeter 並下載以利接下來的測試使用。完成安裝後,如果看不習慣英文介面,可以透過左上角的 ”選項” 自行調整介面語言。

我們可以在計畫內增加一個執行緒群組用於此次的測試,如附圖所示:
https://ithelp.ithome.com.tw/upload/images/20241013/2016875306W1Sxg9Kb.jpg

接著於執行緒群組內新增HTTP請求,如附圖所示:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753ZxAIudNVXp.jpg

我們可以在HTTP請求內設定 API 的路徑以及 API 的方法,並設定每次請求要累加值為多少,假設目前每次發請求都固定累加的值 10,如附圖所示:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753HBSxzNRwg4.jpg

在執行緒群組的部分可以設定這個執行緒存組總共要執行幾次,如附圖所示:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753ougiBGBfUW.jpg

假設我們目前的執行緒群組設定的執行緒數量為 100 且群組內定義了一個HTTP請求,則表示該請求會執行 100 次。

實際執行後,可以透過 Redis 的 GUI 工具來查詢 指定 Key 的值目前為何,如附圖所示:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753zqrphsGNQV.jpg

因為在 application.properties 內設定的資料庫為 1 ,所以需要到編號為 1 的資料庫下查看資料。從上圖會發現不論是 Key 或是 Value 明明在 set 資料時都是以字串的方式儲存,但為甚麼透過 Redis GUI 工具查看時會是不可讀的字串呢 ? 這可能是因為程式在將資料存至 Redis 時使用了 Java 默認的序列化方式(JdkSerializationRedisSerializer),導致儲存的資料不是以純字串的方式呈現。

儘管如此,我們還是可以透過一些簡單的方式來確認目前 Redis 特定鍵的值為何,例如再寫一支查詢 API:

public String query(String key) {
    Object object = redisTemplate.opsForValue().get(key);
    return object.toString();
}

我們可以透過 get() 方法並指定 Key 來進行查詢。並於 Controller 中定義一個查詢的 API接口:

@RequestMapping(value = "/query", method = {RequestMethod.POST})
@ResponseBody
public String queryScore(
        @RequestParam String key
) {
    String result = redisOperatorService.query(key);

    return "目前累積分數 : " + result;

}

透過 Postman 的查詢結果如下:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753fL2ATLKmvB.jpg

根據壓測的設定,我們預期 incrKey 內的值應為 1,000 (10 * 100次),但透過 GUI 工具來檢視後發現與我們預想的結果不同,為甚麼會是這樣的結果呢?

這又要回到 Day23 我們在探討鎖的概念和目的時有提到的一個重點 - 鎖的目的在於當多執行緒同時訪問某一個資源時,防止資料發生不一致或衝突。在上述範例中並未實踐鎖的機制,顯而易見的遇到了資料不一致的問題。

我們該如何解決這個問題呢?

在探討鎖時也有提到可於程式內加鎖,例如透過 Java 原生的 synchronized 方法,這是一個最簡單也是最常用的加鎖方式,它可以應用於方法或是程式內的某個區塊,確保同一時間只有一個執行緒可以進入被鎖定的區域。調整後的方法如下:

public synchronized void incr(int score) {
    try {
        int currentValue = 0;
        Object object = redisTemplate.opsForValue().get(REDIS_INCR_KEY);
        if (object == null) {
            currentValue = add(currentValue, score);
        } else {
            currentValue = add(Integer.valueOf(object.toString()), score);
        }
        redisTemplate.opsForValue().set(REDIS_INCR_KEY, String.valueOf(currentValue));
        logger.info("currentValue = " + currentValue);

    } catch (Exception e) {
        throw new RuntimeException("Failed to increment value in Redis", e);
    }
}

調整後,透過原本 JMeter 執行100次 API 請求再查詢一次結果如下:
https://ithelp.ithome.com.tw/upload/images/20241013/20168753mD97DgIOXg.jpg

此方法似乎確實解決了同步問題,但假設程式如果被分散在多個節點或容器時,程式內的鎖就沒辦法確保跨節點存取資源的資料一致性。舉例來說,我們把這次實作的程式碼內容寫到了另一個專案內,並且同時對兩支程式進行大量的 API 請求,因為這兩支程式都會向 Redis 內的同個資料進行存取,所以可能又會引發相同的問題。如果真的想要杜絕此問題的發生,需要引入分散式鎖,這種鎖可以於多個應用程式服務間協調資源的訪問,確保資料的一致性。

如果想要實作分散式鎖的功能,我們需要先於 pom.xml 內引入依賴項目:

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version><!-- 請自行選擇適用版本 --></version>
</dependency>

Redisson 是一個 Redis 的函式庫,它不只提供了與 Redis 的基本操作功能,還提供了一些擴充功能(例如分散式鎖),使得分散式應用系統在使用Redis 上更方便、更powerful。

使用 Redisson 實現分散式鎖的方法如下:

public void incr(int score) {
    RLock rLock = redissonClient.getFairLock("redissonIncrFairKey");
    try {
        boolean isLocked = rLock.tryLock(15, 10, TimeUnit.SECONDS);
        if (isLocked) {
            try {
                int currentValue = 0;
                Object object = redisTemplate.opsForValue().get(REDIS_INCR_KEY);
                if (object == null) {
                    currentValue = add(currentValue, score);
                } else {
                    currentValue = add(Integer.valueOf(object.toString()), score);
                }
                redisTemplate.opsForValue().set(REDIS_INCR_KEY, String.valueOf(currentValue));
                logger.info("currentValue = " + currentValue);
            } finally {
                rLock.unlock();
            }
        }
    } catch (Exception e) {
        throw new RuntimeException("Failed to increment value in Redis", e);
    }
}

使用 RedissonClient 取一把名為 “redissonIncrFairKey” 的公平鎖,可以想像成這把鎖在 Redis 中是唯一的名稱,當應用程式成功取得鎖後,才可以對 Redis 指定鍵值的資料進行存取。

透過tryLock() 方法嘗試獲得鎖,並嘗試在指定的時間內取得鎖以及設定成功取得鎖後多久要釋放鎖(可持有鎖的時間)。成功獲取鎖後,我們可以進行相應的資料操作,並在完成後確保鎖有被釋放。


上一篇
Day28 - Spring Boot 整合 Redis(上)
下一篇
Day30 - 完賽感言
系列文
這些年SpringBoot實戰開發教會我的事30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言